/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package formula;

import formula.node.Node;
import formula.node.OperationNode;
import formula.node.OperationNodeType;
import formula.node.ValueNode;
import geneticprogramming.problem.GEProblem;
import geneticprogramming.problem.Individual;
import java.util.ArrayList;
import java.util.Hashtable;

/**
 *
 * @author CJ
 */
public class FormulaProblem implements GEProblem{


    protected static final double FITNESS_VARABLE_ONE = 10;
    protected static final double FITNESS_VARABLE_TWO = 50;
    protected static final double FITNESS_VARABLE_THREE = 100;

    protected FormulaProblemParams params;

    public FormulaProblem(FormulaProblemParams params){
        this.params = params;
    }

    public Individual[] getRandomPopulation(int populationNum){
        FormulaIndividual[] result = new FormulaIndividual[populationNum];

        for(int i = 0 ; i < result.length; ++i){
            //TODO Generate Individual
            //Root node must always be an operation
            OperationNode root = new OperationNode(Node.getRandomNode(params, 1), Node.getRandomNode(params, 1),
                    OperationNodeType.getRandomOperation(), 0);
            FormulaIndividual nextIndividual = new FormulaIndividual(root);
            simplfyState(nextIndividual);
            result[i] = nextIndividual;

        }

        return result;
    }

    public Individual mutate(Individual individual){
        Node currentNode = (Node)(individual.getState());
        while((int)(Math.random() * 2) == 1){
            //Where should we stop???

            if(currentNode instanceof ValueNode){
                //Exit, no tree left
                break;
            }

            //else find the next node in the tree
            switch((int)(Math.random() * 2)){
                case 0:
                    currentNode = currentNode.getLeft();
                    break;
                default:
                    currentNode = currentNode.getRight();
            }
        }

        //Now to find what the muting node is of what class
        //We can modify directly
        if(currentNode instanceof ValueNode){
            ((ValueNode)currentNode).setValue((ValueNode)Node.getRandomValueNode(params, currentNode.getDepth()));
        }else{
            ((OperationNode)currentNode).setType(OperationNodeType.getRandomOperation());
        }
        
        return individual;
    }

    public Individual mate(Individual invOne, Individual invTwo){
        if(invOne.hashCode() == invTwo.hashCode()){
            if(invOne.getFitness() > invTwo.getFitness())
                return invOne.clone();
            return invTwo.clone();
        }

        Node rootOne = (Node)(invOne.getState());
        Node rootTwo = (Node)(invTwo.getState());
        rootOne = rootOne.clone();
        rootTwo = rootTwo.clone();


        if(!(rootOne instanceof OperationNode) || !(rootTwo instanceof OperationNode)){
            throw new IllegalArgumentException("ParentNode is not OperationNode");
        }

        OperationNode parentOne = (OperationNode)randomSelectParent(rootOne);
        OperationNode parentTwo = (OperationNode)randomSelectParent(rootTwo);

        //Find which one will have the main root
        if((int)(Math.random() * 2) == 0){
            //One has main root, now to find which side
            if((int)(Math.random() * 2) == 0){
                //Now find with side of the other parent
                parentOne.setLeft(((int)(Math.random() * 2) == 0)? parentTwo.getLeft(): parentTwo.getRight());
            }else{
                //Ditto
                parentOne.setRight(((int)(Math.random() * 2) == 0) ? parentTwo.getLeft() : parentTwo.getRight());
            }
            FormulaIndividual result = new FormulaIndividual(rootOne);
            simplfyState(result);
            if(findMaxDepth(rootOne, 0) > params.maxDepth)
                return returnMoreFit(invOne, invTwo);
            return result;
        }else{
            //Two has main root, now to find which side
            if((int)(Math.random() * 2) == 0){
                //Now find which side of the other parent
                parentTwo.setLeft(((int)(Math.random() * 2) == 0)? parentOne.getLeft(): parentOne.getRight());
            }else{
                //Ditto
                parentTwo.setRight(((int) (Math.random() * 2) == 0) ? parentOne.getLeft() : parentOne.getRight());
            }
            FormulaIndividual result = new FormulaIndividual(rootTwo);
            simplfyState(result);
            if(findMaxDepth(rootTwo, 0) > params.maxDepth)
                return returnMoreFit(invOne, invTwo);
            return result;
        }


    }

    public double getFitness(Individual individual){
        Node rootNode = (Node)individual.getState();
        double totalFitness = 0;
        Hashtable<String, Double> varableValues = new Hashtable<String, Double>();

        //Copy array because its destructive
        ArrayList<String> varablesNames = new ArrayList<String>();
        for(int i = 0; i < params.varableNames.size(); ++i)
            varablesNames.add(params.varableNames.get(i));

        //Iterate through all the varables, through recursion
        totalFitness = recursiveVarableCalculate(rootNode, varablesNames, varableValues);
        
        return totalFitness;
    }

    public double getPerfectFitness(){
        return params.maxFitness;
    }

    public int getMutationProb(double averageFitness){
        return 50;
    }

    public double recursiveVarableCalculate(Node rootNode, ArrayList<String> varables, Hashtable<String, Double> varableValues){
        //Base Case
        if(varables.size() < 1){
            //Calculate Deviation from perfect
            double ideal = params.rootNodeIdeal.eval(varableValues);//wowowowow THE OVERHEAD!
            double eval = rootNode.eval(varableValues);
            //Below makes it sure to be positive AND < 1
            double error = (ideal > eval)? (ideal - eval) : (eval - ideal);
            double percentError = error/ideal;

            double fitness = params.maxFitness * (1 - percentError);
            if(percentError > 1 || fitness < 1){
                //The error is just so much. O.o
                return 1;
            }
            return fitness;
        }

        double totalFitness = 0;

        //Store key
        String key = varables.get(0);
        //Remove key for recursion
        varables.remove(key);

        // 1) FITNESS VARABLE 1
        //Place First Value
        varableValues.put(key, FITNESS_VARABLE_ONE);
        //Call recursive
        totalFitness += recursiveVarableCalculate(rootNode, varables, varableValues);

        // 2) FITNESS VARABLE 2
        //Place Second Value
        varableValues.put(key, FITNESS_VARABLE_TWO);
        //Call recursive
        totalFitness += recursiveVarableCalculate(rootNode, varables, varableValues);

        // 3) FITNESS VARABLE 3
        //Place third Value
        varableValues.put(key, FITNESS_VARABLE_THREE);
        //Call recursive
        totalFitness += recursiveVarableCalculate(rootNode, varables, varableValues);

        //Return Average
        return totalFitness/3;
    }

    public Node randomSelectParent(Node rootNode){
        //Make sure we're an operationNode
        if(!(rootNode instanceof OperationNode)){
            throw new IllegalArgumentException("Node is not OperationNode");
        }

        //Make life easier
        OperationNode node = (OperationNode)rootNode;

        //First Find if we can go anywhere
        if((node.getLeft() instanceof ValueNode)
                && (node.getRight() instanceof ValueNode)){
            //We stuck, return ourselves
            return node;
        }

        //Now find if we should continue
        if((int)(Math.random() * 2) == 0){
            //Yes! Continue
            switch((int)(Math.random() * 2)){
                case 0:
                    if(node.getLeft() instanceof OperationNode){
                        //Return this one, if its not return other
                        return randomSelectParent(node.getLeft());
                    }else{
                        return randomSelectParent(node.getRight());
                    }
                default:
                    if(node.getRight() instanceof OperationNode){
                        //Ditto
                        return randomSelectParent(node.getRight());
                    }else{
                        return randomSelectParent(node.getLeft());
                    }
                  
            }
        }else{
            //No, don't even think about it
            return node;
        }
    }

    protected Individual returnMoreFit(Individual one, Individual two){
        if(one.getFitness() > two.getFitness())
            return one.clone();
        return two.clone();
    }

    protected static int findMaxDepth(Node rootNode, int currentDepth){
        if(rootNode instanceof ValueNode)
            return currentDepth;
        return Math.max(findMaxDepth(rootNode.getLeft(), currentDepth + 1), findMaxDepth(rootNode.getRight(), currentDepth + 1));
    }

    /**
     * Destructive simply of state of input individual.  Simplfying the formula
     * expression.
     * @param input
     */
    public static void simplfyState(Individual input){
        Node rootNode = (Node)(input.getState());
        //There is no parent of the rootNode
        //This method as mentioned above is DESTRUCTIVE
        recursiveSimplfy(rootNode, null);
    }

    protected static void recursiveSimplfy(Node currentNode, Node parentNode){
        
        //If we are a ValueNode, there is not point in returning.
        if(currentNode instanceof ValueNode)
            return;

        OperationNode node = (OperationNode)currentNode;

        //Recursive on both lower nodes
        recursiveSimplfy(node.getLeft(), currentNode);
        recursiveSimplfy(node.getRight(), currentNode);

        //Now do simplify check
        if(node.getLeft().reducable()
                && node.getRight().reducable()
                && parentNode != null){
            //Simplify
            //Note: We should be able to pass a null value down.
            double newValue = node.eval(null);
            ValueNode newNode = new ValueNode(newValue, null, currentNode.getDepth());

            //Change my parent
            //I have to find which one I am though
            if(parentNode.getRight().hashCode() == currentNode.hashCode()){
                ((OperationNode)parentNode).setRight(newNode);
            }else if(parentNode.getLeft().hashCode() == currentNode.hashCode()){
                ((OperationNode) parentNode).setLeft(newNode);
            }else{
                throw new IllegalArgumentException("Unable to match child node to parent.");
            }
        }
    }
}
